package com.dliyun.demo;


import lombok.extern.slf4j.Slf4j;

import java.lang.management.ManagementFactory;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * =============================================================================
 * ==== name bit Byte number len max-len
 * -----------------------------------------------------------------------------
 * ---- mac 1 48 6 281,474,976,710,655 mac 2 49 562,949,953,421,311 9
 * 11,694,146,092,834,140 ------ MAC RAN(12) * E15 + mac
 * -----------------------------------------------------------------------------
 * ---- time 0 13,054,752,000 (from 2011.05.16 100ms) time 1 28,833,984,000
 * (from 2061.05.16 100ms) 7 3,142,742,836,021 ------ TIME RAN(32) * E11 + time
 * -----------------------------------------------------------------------------
 * ---- pid 16 2 65535 3 226,980 ------ PID pid
 * -----------------------------------------------------------------------------
 * ---- seq 26 67,108,864 5 844,596,300 ------ SEQ RAN(8) * E8 + seq
 * -----------------------------------------------------------------------------
 * ---- ip 32 4 4,294,967,296 6 51,520,374,361 ------ IP RAN(5) * E10 + ip
 * =============================================================================
 * ==== *
 *
 * @author jtoms
 */
@Slf4j
public class Token {

	private final static char[] BASE61 = "3ibqZmxNpXOjlHIPQVSTUhWCBgFDctuYAzKd1fenyE6957rwMaLJ2kR4vso8G".toCharArray();
	private final static int LEN = BASE61.length;
	private final static int DATA_LENGTH = 30;
	private final static int TOKEN_LENGTH = 33;
	private final static int[] BASE61_MAPPING = {32, 24, 23, 27, 41, 26, 60, 13, 14, 51, 34, 50, 48, 7, 10, 15, 16, 54, 18, 19, 20, 17, 22, 9, 31, 4, 49, 2, 28, 35, 38, 37, 25,
			21, 1, 11, 53, 12, 5, 39, 58, 8, 3, 46, 57, 29, 30, 56, 47, 6, 40, 33, 36, 52, 0, 55, 44, 42, 45, 59, 43};

	private final static long MAC_ADDR = mac();

	private final static int PID = Integer.parseInt(ManagementFactory.getRuntimeMXBean().getName().split("@")[0]);

	private final static AtomicInteger SEQ = new AtomicInteger((int) (System.currentTimeMillis() / 1000));

	private final static AtomicInteger[] SEN = {new AtomicInteger((int) (System.nanoTime())), new AtomicInteger((int) (System.nanoTime())),
			new AtomicInteger((int) (System.nanoTime())), new AtomicInteger((int) (System.nanoTime())), new AtomicInteger((int) (System.nanoTime()))};

	private long mac;

	private int pid;

	private int sequence;

	private long tokenTime;

	private String clientIp;


	public Token() {
	}

	public Token(String clientIp) {
		this.mac = MAC_ADDR;
		this.tokenTime = System.currentTimeMillis() / 100;
		this.pid = PID;
		this.clientIp = clientIp;
		this.sequence = SEQ.incrementAndGet() & 0x3ffffff;
	}

	public Token(long mac, long tokenTime, int pid, int sequence, String clientIp) {
		super();
		this.mac = mac;
		this.tokenTime = tokenTime;
		this.pid = pid;
		this.sequence = sequence;
		this.clientIp = clientIp;
	}

	public static Token init(String clientIp) {
		return new Token(clientIp);
	}

	public static boolean checkToken(String value) {
		if (value == null || value.length() != Token.TOKEN_LENGTH) {
			return false;
		}
		char[] chs = value.toCharArray();
		for (int i = 0; i < TOKEN_LENGTH; i++) {
			if (getNumber(chs[i]) < 0) {
				log.warn("value index [" + i + "] is invalid encode character [" + chs[i] + "], token: [" + value + "]");
				return false;
			}
		}
		char[] hash = hash(chs, 0, DATA_LENGTH, TOKEN_LENGTH - DATA_LENGTH);
		for (int i = DATA_LENGTH, j = 0; i < chs.length; i++, j++) {
			if (chs[i] != hash[j]) {
				log.warn("value hash is invalid, value hash: [" + new String(chs, DATA_LENGTH, TOKEN_LENGTH - DATA_LENGTH) + "], calculate hash: [" + new String(hash)
						+ "], token: [" + value + "]");
				return false;
			}
		}
		return true;
	}

	public static Token decodeTokenString(String value) {
		if (log.isDebugEnabled()) {
			log.debug("start check TOKEN_ID: " + value);
		}
		if (!Token.checkToken(value)) {
			log.warn("check token failed");
			return null;
		}
		char[] chs = value.toCharArray();
		int offset = 0;
		long mac = Token.decodeNode(chs, offset, 9, 1000000000000000L);
		if (mac < 0) {
			log.warn("token mac is invalid, mac: [" + new String(chs, 0, 9) + "]token: [" + value + "]");
			return null;
		}
		long tokenTime = Token.decodeNode(chs, (offset += 9), 7, 100000000000L);
		if (tokenTime < 0) {
			log.warn("token time is invalid, tokenTime: [" + new String(chs, offset, 7) + "]token: [" + value + "]");
			return null;
		}
		int pid = (int) Token.decodeNode(chs, (offset += 7), 3, 0);
		if (pid < 0 || pid > 226980) {
			log.warn("token pid is invalid, pid: [" + new String(chs, offset, 3) + "], token: [" + value + "]");
			return null;
		}
		int sequence = (int) Token.decodeNode(chs, (offset += 3), 5, 100000000L);
		if (sequence < 0) {
			log.warn("token sequence is invalid, sequence: [" + new String(chs, offset, 5) + "], token: " + value);
			return null;
		}
		long clientIp = Token.decodeNode(chs, (offset += 5), 6, 10000000000L);
		if (clientIp < 0 || clientIp > 0xffffffffL) {
			log.warn("token clientIp is invalid, clientIp: [" + new String(chs, offset, 6) + "], number client ip: [" + Long.toHexString(clientIp) + "], token: " + value);
			return null;
		}
		return new Token(mac, tokenTime, pid, sequence, Token.toClientIp(clientIp));
	}

	static char[] hash(char[] chs, int offset, int length, int hashLength) {
		char[] hash = new char[hashLength];
		int p = 0;
		for (int i = offset, k = offset + length; i < k; i++) {
			p = p * 31 + chs[i];
		}
		p &= Integer.MAX_VALUE;
		for (int i = 0; i < hashLength; i++, p /= LEN) {
			hash[i] = BASE61[p % LEN];
		}
		return hash;
	}

	static String encode(long n, int max) {
		char[] chs = new char[max];
		int k = 0;
		while (n > 0) {
			chs[k++] = BASE61[(int) (n % LEN)];
			n /= LEN;
		}
		while (k < max) {
			chs[k++] = BASE61[0];
		}
		return new String(chs);
	}

	static Token parse(String token) {
		if (token == null || token.length() != TOKEN_LENGTH) {
			return null;
		}
		char[] chs = token.toCharArray();
		for (int i = 0; i < TOKEN_LENGTH; i++) {
			if (getNumber(chs[i]) < 0) {
				return null;
			}
		}
		char[] hash = hash(chs, 0, DATA_LENGTH, TOKEN_LENGTH - DATA_LENGTH);
		for (int i = DATA_LENGTH, j = 0; i < chs.length; i++, j++) {
			if (chs[i] != hash[j]) {
				log.warn("token: " + token + " hash error");
				return null;
			}
		}
		return parseTokenString(chs);
	}

	private static int next(int mod, int offset) {
		return (SEN[offset].incrementAndGet() & 0x7fffffff) % mod;
	}

	private static Token parseTokenString(char[] chs) {
		Token token = new Token();
		int offset = 0;
		token.mac = decodeNode(chs, offset, 9, 1000000000000000L);
		token.tokenTime = decodeNode(chs, (offset += 9), 7, 100000000000L);
		token.pid = (int) decodeNode(chs, (offset += 7), 3, 0);
		token.sequence = (int) decodeNode(chs, (offset += 3), 5, 100000000L);
		token.clientIp = toClientIp(decodeNode(chs, offset + 5, 6, 10000000000L));
		return token;
	}

	private static int encodeNode(char[] chs, int ran, long value, int offset, int len) {
		return encodeNode(chs, ran, value, offset, len, 0, 0);
	}

	private static int encodeNode(char[] chs, int ran, long value, int offset, int len, int random, long randomBase) {
		if (random > 0) {
			value = (long) next(random, ran) * randomBase + value;
		}
		for (int i = 0; i < len; i++) {
			chs[offset + i] = BASE61[(int) (value % LEN)];
			value /= LEN;
		}
		return offset + len;
	}

	static long decode(String str) {
		char[] chs = str.toCharArray();
		long p = 0;
		for (int i = chs.length - 1; i >= 0; i--) {
			int k = getNumber(chs[i]);
			if (k < 0) {
				return -1;
			}
			p = p * LEN + k;
		}
		return p;
	}

	public static long decodeNode(char[] chs, int offset, int len, long randomBase) {
		long n = 0;
		for (int i = offset + len - 1; i >= offset; i--) {
			int k = getNumber(chs[i]);
			if (k < 0) {
				return -1;
			}
			n = n * LEN + k;
		}
		return randomBase < 1 ? n : n % randomBase;
	}

	private static long parseIp(String ip) {
		char[] chs = ip.toCharArray();
		long t = 0;
		int n = 0;
		for (char ch : chs) {
			if (ch == '.') {
				t = (t << 8) | (n & 0xff);
				n = 0;
				continue;
			}
			if (ch >= '0' && ch <= '9') {
				n = n * 10 + (ch - '0');
			}
		}
		if (n != 0) {
			t = (t << 8) + (n & 0xff);
		}
		return t & 0xffffffffL;
	}

	static int getNumber(char c) {
		if (c >= 'A' && c <= 'Z') {
			return BASE61_MAPPING[c - 'A'];
		}
		if (c >= 'a' && c <= 'z') {
			return BASE61_MAPPING[c - 'a' + 26];
		}
		if (c >= '1' && c <= '9') {
			return BASE61_MAPPING[c - '1' + 52];
		}
		return -1;
	}

	public static long mac() {
		byte[] bys = null;
		try {
			for (Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces(); e.hasMoreElements(); ) {
				byte[] mac = e.nextElement().getHardwareAddress();
				if (mac != null && mac.length == 6) {
					bys = mac;
					break;
				}
			}
		} catch (SocketException e) {
			log.error("can not get local machine MAC address, generate random 47bit simulate MAC", e);
		}
		if (bys == null) {
			bys = new byte[7];
			new Random().nextBytes(bys);
			bys[0] &= 1;
		}
		long mac = 0;
		for (byte b : bys) {
			mac = mac * 256L + (b & 0xff);
		}
		mac &= 0x1ffffffffffffL;
		log.info("current MAC number is: " + mac + ", HEX: " + Long.toHexString(mac));
		return mac;
	}

	public static String toClientIp(long ip) {
		return ((ip >> 24) & 0xff) + "." + ((ip >> 16) & 0xff) + "." + ((ip >> 8) & 0xff) + "." + (ip & 0xff);
	}

	public long getMac() {
		return mac;
	}

	public int getPid() {
		return pid;
	}

	public int getSequence() {
		return sequence;
	}

	public long getTokenTime() {
		return tokenTime * 100L;
	}

	public String getClientIp() {
		return clientIp;
	}

	public String toTokenString() {
		int offset = 0;
		char[] chs = new char[TOKEN_LENGTH];
		int p = 0;
		offset = encodeNode(chs, p++, mac, offset, 9, 12, 1000000000000000L);
		offset = encodeNode(chs, p++, tokenTime, offset, 7, 32, 100000000000L);
		offset = encodeNode(chs, p++, pid, offset, 3);
		offset = encodeNode(chs, p++, sequence, offset, 5, 8, 100000000L);
		offset = encodeNode(chs, p++, parseIp(clientIp), offset, 6, 5, 10000000000L);
		char[] hash = hash(chs, 0, offset, TOKEN_LENGTH - DATA_LENGTH);
		System.arraycopy(hash, 0, chs, offset, hash.length);
		return new String(chs);
	}

	@Override
	public String toString() {
		return "Token [mac=" + mac + ", tokenTime=" + String.format("%tF %<tT", getTokenTime()) + ", pid=" + pid + ", sequence=" + sequence + ", clientIp=" + clientIp + "]";
	}

	public boolean equals(Token token) {
		if (!this.getClientIp().equals(token.getClientIp())) {
			return false;
		}
		if (this.getMac() != token.getMac()) {
			return false;
		}
		if (this.getPid() != token.getPid()) {
			return false;
		}
		if (this.getSequence() != token.getSequence()) {
			return false;
		}
		return true;
	}
}